<?php
/**
 * Класс-конфигуратор, обрабатывающий путь в формате /anyval/node2/foobar/
 * И собирающий хеш-конфигурацию в соответствии с этим путем
 */
class Structure {
	/* @var array $config */
	protected $config;
	protected $fieldsMap = array(
		'keywords'=>'array',
		'description'=>'string',
		'title'=>'string',
		'widgets'=>'array',
	);
	private $yaml;
	protected $fields = array();

	// операции по обработке значения параметра
	const REPLACE = 0;		// заменяем данным значением
	const APPEND = 10;		// объединяем с тем, что уже есть - приклеиваем в хвост
	const PREPEND = 15;		// объединяем с тем, что уже есть - в начало
	const REMOVE = 20;	// удаляем указанные значения
	const CLEAR = 30;	// удаляем все значения

	// порядок операций соблюдается при парсинге
	// TODO прикинуть, каковы приоритеты этих операций
	private $operationSigns = array(
		''=>self::REPLACE,
		'+'=>self::APPEND,
		'++'=>self::PREPEND,
		'-'=>self::REMOVE,
	);

	function __construct($configFile){
		$this->yaml = new Spyc();
		$ext = CFileHelper::getExtension($configFile);
		switch($ext){
			case 'php':
				$this->config = require $configFile;
				break;
			case 'yml':
			case 'yaml':
				$this->yaml->loadFile($configFile);
				$this->config = $this->yaml->result;
				break;
		}
		if(!is_array($this->config)){
			throw new Exception('Config error:it\'s not array');
		}
		$this->init();
	}

	/**
	 * Наполняет поля начальными значениями, только с 1го уровня
	 * @return void
	 */
	public function init() {
		$this->applyConfig();
	}

	/**
	 * Текущее состояние в виде массива
	 * @return array
	 */
	public function toArray() {
		$ret = array();
		foreach($this->fieldsMap as $name=>$type){
			$value = $this->getField($name);
			if($value !== null)
				$ret[$name] = $value;
		}
		return $ret;
	}

	public function toYaml() {
		return $this->yaml->dump($this->toArray(), 4, 80);
	}

	/**
	 * Массив загруженной конфигурации
	 * @return array|mixed
	 */
	public function getConfig() {
		return $this->config;
	}

	/**
	 * Обработка адреса с занесением результатов в соотв. поля
	 * @param $path
	 * @return void
	 */
	public function parsePath($path, $config = false) {
		if($config===false){
			$config = $this->config;
		}
		// остальное повторяется на всех уровнях
		//TODO поддержка любых маршрутов, типа /path/to/one/dir
		//TODO поддержка RegExp, типа #path/any(.+)#
		$pathParts = explode('/', $path);
		$currentPathPart = array_shift($pathParts);
		// исследуем текущий узел
		if(isset($config['/'.$currentPathPart])){
			$currentConfig = $config['/'.$currentPathPart];
			$this->applyConfig($currentConfig);
			// если есть вложенный путь - опускаемся в него
			if(count($pathParts)){
				$nextPath = implode('/', $pathParts);
				$this->parsePath($nextPath, $currentConfig);
			}
		}

//		return $result;
	}

	/**
	 * Производит разбор данного конфига, на 1 уровне,
	 * после чего переназначает поля
	 * @param bool $config
	 * @return void
	 */
	private function applyConfig($config=false) {
		if($config===false){
			$config = $this->config;
		}
		foreach($this->fieldsMap as $name=>$type){
			$this->applyParam($config, $name);
		}
	}

	/**
	 * Применяет параметр из конфига к текущему состоянию,
	 * прорабатывая все операции
	 * @throws Exception
	 * @param $config
	 * @param $name
	 * @return void
	 */
	protected function applyParam($config, $name) {
		// перебираем операции
		foreach($this->operationSigns as $sign=>$operation){
			if(isset($config[$sign.$name])){
				$this->applyOperation($operation, $name, $config[$sign.$name]);
			}
		}
	}

	/**
	 * Применяем очередную операцию к указанному полю, используя указанное значение
	 * @param $operation
	 * @param $name
	 * @param $value
	 * @return void
	 */
	protected function applyOperation($operation, $name, $value) {
		$type = $this->fieldsMap[$name];
		$oldValue = $this->getField($name);
		$value = $this->normalizeValue($value, $type);
		switch($operation){
			case self::REPLACE:
				if(!$value){
					$value = null;
				}
				break;
			case self::APPEND:
				switch($type){
					case 'string':
						if($oldValue !== null){
							$value = $oldValue . $value;
						}
						break;
					case 'array':
						$value = array_merge($oldValue, $value);
						break;
				}
				break;
			case self::PREPEND:
				switch($type){
					case 'string':
						if($oldValue !== null){
							$value = $value . $oldValue ;
						}
						break;
					case 'array':
						$value = array_merge($value, $oldValue);
						break;
				}
				break;
			case self::REMOVE:
				if(!$value){
					$value = null;
				}else{
					switch($type){
						case 'string':
							$value = str_replace($oldValue, '', $value);
							break;
						case 'array':
							foreach($value as $valRow){
								$ind = array_search($valRow, $oldValue);
								unset($oldValue[$ind]);
							}
							$value = $oldValue;
							break;
					}
				}
				break;
		}
		$this->setField($name, $value);
	}

	/**
	 * Приводим переданное значение к пригодному для обработки виду
	 * @param $value
	 * @param $type
	 * @return array|string
	 */
	private function normalizeValue($value, $type) {
		switch($type){
			case 'string':
				$value = (string) $value;
				break;
			case 'array':
				if(gettype($value) == 'string'){
					if(trim($value) == ''){
						$value = array();
					}else{
						// TODO настраивать разделитель
						$arValue = explode(',', $value);
						if(empty($arValue)){
							$value = array($value);
						}
						$value = array_map('trim', $arValue);
					}
				}
				break;
		}
		return $value;
	}

	public function getField($name){
		return $this->fields[$name];
	}
	public function setField($name, $value){
		$this->fields[$name] = $value;
	}
}
